Article text, originally published as "Demagnetizing Static Variables," Programmer's Journal 7.1, 1989, pp 42-52. ----- Copyright (c) 1989 by Tom Swan. All rights reserved. Limited permission granted to use programming in compiled form only. ----- Author's note: This information and the accompanying programs are compatible with Turbo Pascal 5.5. I wrote the article before this version was available, so any references to page numbers in the Borland references may be incorrect. Demagnetizing Static Variables by Tom Swan "Modifying typed constants (alias static variables) stored in Turbo Pascal 4.0 and 5.0 compiled EXE code files is hairy business--but it can be done if you're careful." Static variables, or typed constants as Turbo Pascal calls them, stick to compiled code like magnets to iron. Declared as constants but compiled as variables, typed constants are stored on disk inside a program's EXE code file. Because typed constants have predefined values, they eliminate wasteful initializations of global variables at run time. Despite this advantage, however, after compiling, there's no easy way to change a program's typed constants--for example, to let people make keyboard assignments, select window colors, and modify other default settings. A telephone call from a reader prompted me to think of several possible solutions to this problem. I knew I'd have to locate the typed constants embedded in the compiled code file and modify only the necessary bytes, storing the result back on disk with all other bytes intact. Mucking around with EXE disk files gives me the heebie jeebies, so the code would have to be reasonably immune to changes in future compiler versions. The program should run fast, be easy to modify, work with both small and large applications, wash the floor, dust the shelves, and take out the garbage. The following describes the results of my experiments, largely successful although I'm still doing the floor, shelves, and garbage by hand. After a brief refresher course on typed constants as stored on disk and in memory, I'll explain how to use the accompanying library unit to write custom installation programs. Also included are demonstration programs that you can use as starting places for your own projects. A Needle in a Byte Stack Finding typed constants in memory and on disk isn't nearly as difficult as finding a needle in you know what. Let's start with values in memory, as these are the easiest to locate. The first typed constant in memory is always stored at offset zero in the data segment. (There is only one data segment in a Turbo Pascal program.) Suppose you have these declarations: const aword : word = ( 1024 ); achar : char = ( 'X' ); abyte : byte = ( 15 ); The typed constant aword is stored at DSeg:0000, occupying two bytes. After this comes achar at DSeg:0002. Next, abyte follows at DSeg:0003. DSeg is a predeclared constant equal to the value of register DS. Rather than calculate these offsets manually, use the Ofs function to find the address of any typed constant. For example, this displays the offset addresses of the three typed constants in memory: writeln( ' aword=', ofs(aword), ' achar=', ofs(achar), ' abyte=', ofs(abyte) ); In Turbo Pascal 4.0, typed constant values follow each other with no wasted bytes between. As a result, lone byte values can lead to extra CPU machine cycles by forcing words to begin at odd addresses. If you insert a fourth typed constant word between achar and abyte, the new value resides at DSeg:0003, making the CPU work harder to access the value. To fix this problem, Turbo Pascal 5.0 provides a new switch {$A+} to align typed constants and other variables on even addresses, letting the CPU access 16- bit quantities as quickly as possible. (Word alignment offers no advantage on PCs with 8088 processors.) The default is {$A-} for no alignment. We Are Not Alone Your typed constants are not alone in memory. The System unit, containing Turbo Pascal's runtime library and linked into every compiled program, declares its own set of typed constants. (The 5.0 Reference Guide lists these on page 125.) In memory, items in the data segment follow this order: 1. Program typed constants 2. Unit typed constants 3. System typed constants 4. Global variables The main program's typed constants are always first, beginning at offset zero. Next come any typed constants declared in units that the program lists in a USES declaration. Last come the System unit's typed constants, followed by the program's global variables. Together, these four items make up the program's data segment, which can be as large as 64K. Items 1-3 are stored in the compiled EXE code file. Memory for global variables (item 4) is allocated at run time--global variables are never stored in the disk code file. Smart Linker; Dumb Programmer Turbo Pascal's smart and crafty linker, which attempts to optimize compiled code, requires you to refer to at least one typed constant declared in your program and in all units. If you don't, the linker realizes you aren't using the typed constants and does not reserve space for them in the code! While writing codeless test programs to determine where typed constants are stored, it took me longer than I care to reveal to realize the linker was "helping" me by throwing my constants away. The following short program demonstrates how Turbo's smart aleck linker can get you into trouble as it did me: program test; const atc:char=('a'); begin writeln( atc ); writeln( char( ptr(dseg,0 )^) ) end. The program declares one typed constant, atc. Two writeln statements display atc's value, the letter 'a'. The first writeln refers to atc by name. The second refers to atc the hard way, dereferencing a pointer to DSeg:0000 and recasting as type Char, admittedly the long way around a simple job. Take out the first writeln statement and guess what happens. The second writeln no longer displays 'a'! Turbo Pascal throws out atc because it doesn't realize that the ptr expression refers to the typed constant. Obviously, you won't usually refer to typed constants this way, but you might write a program that expects another module to reference a typed-constant array or large record at DSeg:0000. Don't do this. Turbo Pascal does not save typed constants unless you refer by name to at least one in each module. The same rule goes for typed constants in units. Either the unit code or another module must refer to at least one typed constant by name or the linker throws these babies out with the bathwater. Sniffing for Typed Constants Turbo Pascal stores typed constants as the last bytes in a disk code file. At run time, the bytes are copied to the start of the program's data segment in memory. On disk and in memory, the bytes are paragraph aligned--that is, beginning at an offset in the code file and at an address in memory evenly divisible by 16. The problem of writing an installation program to modify default values in compiled disk code files requires finding the offset to the first byte of the first typed constant. Listing 1 (WINDGLOB.PAS) makes this easy by declaring a marker CBase assigned the string value '@CBASE@'. The program's default settings follow CBase--in this sample, the typed constants WBForeColor to WTitle. Last, a dummy typed constant EBase marks the end of the typed constants area. Ebase's data type and value are unimportant, but CBase must be a string. Because the application and installation program refer to the same values, it's best to declare all typed constants in a separate unit similar to WINDGLOB.PAS. In this example, the unit has no code, although it certainly could. The next step is to write the main application, WINDTEST.PAS in Listing 2. This program lists WINDGLOB in a USES declaration, importing the typed constants from Listing 1. Run WINDTEST to display a simple window with scrolling random characters. Press any key to end the program. The last job is to write the installation program, which modifies the typed constant default values in the compiled WINDTEST.EXE program. Listing 3 (WINDINST.PAS) displays a menu to let you change window colors and type a new window title string. The next section describes how to compile and run the programs. Compiling The Test Programs First compile Listing 4, TCUNIT.PAS, creating TCUNIT.TPU. (With the integrated Turbo Pascal compiler, be sure the Compile menu's Destination option is set to "Disk.") TCUNIT has procedures to help you write your own installation programs and contains a function to locate and modify typed constant values in compiled EXE files. I'll explain how to use the unit in a moment. After compiling TCUNIT, compile WINDGLOB.PAS to WINDGLOB.TPU. Then, compile WINDTEST.PAS and run the test application. Finally, compile WINDINST.PAS and run. Change any of the values listed in the menu. In this no-frills version, you have to specify colors by number even though WINDGLOB.PAS declares the default colors by name, using identifiers declared in the standard Crt unit. Press Q to quit and modify WINDTEST.EXE. Press X to quit and not save changes. Run WINDTEST again to see the effect of your new values. How TCUnit Works Two procedures in TCUNIT, GetWord and GetStr, prompt for word and string values. WINDINST calls these procedures to let you enter new values for various typed constant values. You'll probably want to add your own procedures to prompt for values of other types: such as GetInteger, GetChar, GetReal, and so forth. Use the two procedures listed here as guides. Function ChangesSaved in TCUNIT does all the work of opening the compiled EXE code file, locating the typed constants, and writing the modified values back to disk. See the end of WINDINST for an example call to this function, which requires a file name, the CBase marker string, and the offset addresses of the CBase and EBase typed constants. ChangesSaved performs several operations: * First the EXE file is opened. Parameter fileName must be the name of a compiled EXE file with CBase and EBase typed constants around the values to be modified, as shown in WINDGLOB.PAS. * Next, function FoundCBase returns true if the CBase marker string is found in the EXE code file. If so, FoundCBase returns the byte offset to this string. If not, FoundCBase returns false, causing an error message to be displayed. * If CBase is found, procedure SaveData copies the in-memory typed constants to the compiled EXE code file, overwriting only the bytes from CBase to the byte just before EBase. No other bytes are changed in the disk code file. For all this to work correctly, you must be sure to use the identical typed constants in both the main (WINDTEST) and installation (WINDINST) programs. The safest bet is to declare all typed constants in one separate unit (WINDGLOB) and then use the unit in both programs. Writing Your Own Installers Even without fully understanding every detail about how TCUnit operates, it's easy to write your own installation programs. Just follow these steps: 1. You don't have to modify TCUnit, although you might want to add procedures to prompt for specific data types as mentioned earlier. 2. Store all typed constants in a separate unit and compile. Start with CBase and end with EBase as in WINDGLOB.PAS. The number, type, names, and values of typed constants in between CBase and EBase are completely up to you. 3. Write your application, using the unit of typed constants from step 2. Compile to disk. 4. Write your installation program, using WINDINST.PAS as a guide. Add TCUNIT and your typed constants unit to the program's USES statement. Call ChangesSaved to write modified values to your program's code file. By the way, you can also use WINDINST to modify its own code file, WINDINST.EXE. Doing this displays the modified settings the next time you run the installation program. Also, the CBase marker string (see WINDGLOB.PAS) can be anything you like--it doesn't have to be '@CBASE@' as it is here. The string must be unique, occurring nowhere else in the code file. You might change CBase to your name or copyright. Then, if someone tries to remove your notice from the code file, the installation program will no longer work. Sneaky, no? Improving the Programs When you run these tests, you'll notice that WINDINST pauses for several seconds while hunting for CBase in the compiled code. There's a simple way to reduce this time. First, remove the (* and *) comment brackets from the two writeln statements inside TCUNIT's FoundCBase function. Recompile WINDINST and press Q. Jot down the reported offset. When I did this, I got 4768, although your number is likely to be different. The value is decimal, not hex. Edit TCUNIT.PAS again and replace the comment brackets in FoundCBase. Then change the double IF statement in ChangesSaved near the end of the program to read: IF err=0 THEN SaveData(f,4768,...) In other words, replace variable offset with the value you noted earlier and remove the call to FoundCBase. Now when you compile and run WINDINST, the disk update takes place almost instantaneously as TCUNIT no longer has to hunt for the offset to CBase. Perform this step only after compiling your main program for the last time. (Is there ever a last time?) Any changes to your typed constants or to the program require you to recalculate the offset in the code file. Using the wrong offset is almost sure to lead to a crash. Although these ideas and test programs are experimental, I'm planning to use them as the basis for an installation program in my Turbo Pascal database system as well as in other programs. In the past, I've found many uses for typed constants, but I'm planning to work them in more frequently, especially now that I have a way to demagnetize their previously all too static values. Listing 1: WINDGLOB.PAS Listing 2: WINDTEST.PAS Listing 3: WINDINST.PAS Listing 4: TCUNIT.PAS ### About the author... Tom Swan writes the monthly PC World columns Star-Dot-Star and Developer's Toolbox. He is a contributing editor to PC World and Programmer's Journal, and is the author of Mastering Turbo Assembler (1989) and Mastering Turbo Pascal 5.5 (Sep 1989) published by Howard W. Sams, Indianapolis. You can contact the author at: Swan Software P. O. Box 206 Lititz, PA 17543 (717) 627-1911 Compuserve ID: 73627,3241 MCI Mail handle: TSWAN